﻿/*  Copyright (c) 2012 William Rogers and James Boud

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

using System;
using SlimDX;
using SlimDX.Direct3D11;
using Microsoft.Xna.Framework.Content;
using JBBRXG11ContentTexture;

namespace JBBRXG11
{
    public class Texture3D : ContentTypeReader<Texture3D>, IDisposable
    {
        protected SlimDX.Direct3D11.Texture3D dxtexture;
        ShaderResourceView dxtextureview;
        int width, height, depth;
        const int elementsize = 4;  // TODO - this assumes a Color surface - needs change to deal with other surface formats
        bool this_wraps_an_external_texture;

        internal SlimDX.Direct3D11.Texture3D DXTexture { get { return dxtexture; } }
        internal ShaderResourceView View { get { return dxtextureview; } }

        /// <summary>
        /// Do not use this constructor, required by the content system
        /// </summary>
        public Texture3D() // Constructor for content system to use to before calling Read - not in XNA API
        {
            dxtexture = null;
            dxtextureview = null;
            this_wraps_an_external_texture = false;  // TODO: need to find out if this disposes correctly or at all
        }

        void MakeTheTexture(GraphicsDevice graphicsDevice, int width, int height, int depth, bool mipMap, Microsoft.Xna.Framework.Graphics.SurfaceFormat format, bool couldberendertarget)
        {
            Texture3DDescription desc;
            desc = new Texture3DDescription();
            desc.Width = width;
            desc.Height = height;
            desc.Depth = depth;
            if (format != Microsoft.Xna.Framework.Graphics.SurfaceFormat.Color)
            {
                throw new Exception("JBBRXG11 - use of formats in Texture3D other than colour not yet implemented");
            }
            desc.Format = SlimDX.DXGI.Format.R8G8B8A8_UNorm;
            if (mipMap)
            {
                throw new Exception("JBBRXG11 - MIP mapping of Texture3D not yet implemented");
            }
            desc.MipLevels = (mipMap ? 0 : 1);
            desc.OptionFlags = 0;
            if (couldberendertarget)
                desc.BindFlags = BindFlags.ShaderResource | BindFlags.RenderTarget;
            else
                desc.BindFlags = BindFlags.ShaderResource;
            desc.CpuAccessFlags = CpuAccessFlags.None;
            desc.Usage = ResourceUsage.Default;

            dxtexture = new SlimDX.Direct3D11.Texture3D(graphicsDevice.DX_Device, desc);
            dxtextureview = new ShaderResourceView(graphicsDevice.DX_Device, dxtexture);

            this.width = width;
            this.height = height;
            this.depth = depth;
            this_wraps_an_external_texture = false;
        }

        /// <summary>
        /// Creates a new instance of this object.
        /// </summary>
        /// <param name="graphicsDevice">A device.</param>
        /// <param name="width">Texture width.</param>
        /// <param name="height">Texture height.</param>
        /// <param name="depth">Texture depth.</param>
        /// <param name="mipMap">True to generate a full mipmap chain; false otherwise.</param>
        /// <param name="format">Data format.</param>
        public Texture3D(GraphicsDevice graphicsDevice, int width, int height, int depth, bool mipMap, Microsoft.Xna.Framework.Graphics.SurfaceFormat format)
        {
            MakeTheTexture(graphicsDevice, width, height, depth, false, Microsoft.Xna.Framework.Graphics.SurfaceFormat.Color, false);
            GraphicsDeviceManager.GetGame(Game.GraphicsDevice).UserContentAdd(this);
        }

        /// <summary>
        /// Gets the depth of this volume texture resource, in pixels. 
        /// </summary>
        public int Depth { get { return depth; } }

        /// <summary>
        /// Gets the height of this texture resource, in pixels.
        /// </summary>
        public int Height { get { return height; } }

        /// <summary>
        /// Gets the width of this texture resource, in pixels.
        /// </summary>
        public int Width { get { return width; } }

        internal static void CalculateLevelSize3D(int level, int texwidth, int texheight, int texdepth, out int levelwidth, out int levelheight, out int leveldepth)
        {
            int lev = level, wide = texwidth, high = texheight, deep = texdepth;
            while (lev > 0)
            {
                if (wide > 1)
                {
                    wide /= 2;
                    high /= 2; if (high < 1) high = 1;
                    deep /= 2; if (deep < 1) deep = 1;
                    lev--;
                }
                else if (high > 1)
                {
                    high /= 2;
                    wide /= 2; if (wide < 1) wide = 1;
                    deep /= 2; if (deep < 1) deep = 1;
                    lev--;
                }
                else if (deep > 1)
                {
                    deep /= 2;
                    wide /= 2; if (wide < 1) wide = 1;
                    high /= 2; if (high < 1) high = 1;
                    lev--;
                }
                else
                {
                    throw new Exception("JBBRXG11: attempt to set texture data to mip level beyond maximum(" + level.ToString() + " >= " + (level - lev).ToString() + ")");
                }
            }
            levelwidth = wide;
            levelheight = high;
            leveldepth = deep;
        }

        /// <summary>
        /// Gets a copy of 3D texture data.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data">Array of data.</param>
        public void GetData<T>(T[] data) where T : struct
        {
            GetData<T>(data, 0, width * height * depth);
        }

        /// <summary>
        /// Gets a copy of 3D texture data, specifying a start index and number of elements.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data">Array of data.</param>
        /// <param name="startIndex">Index of the first element to get.</param>
        /// <param name="elementCount">Number of elements to get.</param>
        public void GetData<T>(T[] data, int startIndex, int elementCount) where T : struct
        {
            throw new Exception("JBBRXG11 Texture3D method not yet implemented");

            // TODO - check if neccessary and then fix to allow for unexpected RowStride in staging array

            //// Assuming here that the data comes from the top mip level
            //if (elementCount != width * height * depth)
            //    throw new Exception("Data array is not the right size for texture data");
            //if (startIndex < 0 || startIndex + elementCount > data.Length)
            //    throw new Exception("Index range does not lie within the data array");

            //// Create a staging buffer to get CPU read access
            //Texture3DDescription desc;
            //SlimDX.Direct3D11.Texture3D dxstaging;
            //desc = dxtexture.Description;
            //desc.BindFlags = BindFlags.None;
            //desc.CpuAccessFlags = CpuAccessFlags.Read;
            //desc.Usage = ResourceUsage.Staging;
            //// TODO: Assumes that T is Color - but don't know how to check that.  Should be ok though
            //// as I don't provide any way of creating textures of any other element kind.
            //desc.MipLevels = 1;
            //dxstaging = new SlimDX.Direct3D11.Texture3D(Game.GraphicsDevice.DX_Device, desc);

            //// Transfer data from the texture3d to the staging buffer
            //// Note - CopyResource seems to require resources of exactly same size and depth, so we
            //// must use CopySubresourceRegion to copy into the single mip level of the staging buffer
            //Game.GraphicsDevice.DX_Device_Context.CopySubresourceRegion(dxtexture, 0,
            //                                                            new ResourceRegion(0, 0, 0, desc.Width, desc.Height, desc.Depth),
            //                                                            dxstaging, 0, 0, 0, 0);

            //// Read data from staging buffer
            //DataBox box = Game.GraphicsDevice.DX_Device_Context.MapSubresource(dxstaging, 0, 0, MapMode.Read, MapFlags.None);
            //box.Data.ReadRange<T>(data, startIndex, elementCount);
            //Game.GraphicsDevice.DX_Device_Context.UnmapSubresource(dxstaging, 0);

            //// Dispose of the unmanaged staging buffer structure
            //dxstaging.Dispose();
        }
        
        /// <summary>
        /// Gets a copy of 3D texture data, specifying a mimap level, source rectangle,
        /// start index, and number of elements.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="level">Mipmap level.</param>
        /// <param name="left">Position of the left side of the box on the x-axis.</param>
        /// <param name="top">Position of the top of the box on the y-axis.</param>
        /// <param name="right">Position of the right side of the box on the x-axis.</param>
        /// <param name="bottom">Position of the bottom of the box on the y-axis.</param>
        /// <param name="front">Position of the front of the box on the z-axis.</param>
        /// <param name="back">Position of the back of the box on the z-axis.</param>
        /// <param name="data">Array of data.</param>
        /// <param name="startIndex">Index of the first element to get.</param>
        /// <param name="elementCount">Number of elements to get.</param>
        public void GetData<T>(int level, int left, int top, int right, int bottom, int front, int back, T[] data, int startIndex, int elementCount) where T : struct
        {
            throw new Exception("JBBRXG11 Texture3D method not yet implemented");
        }

        /// <summary>
        /// Sets 3D texture data.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data">Array of data.</param>
        public void SetData<T>(T[] data) where T : struct
        {
            SetData<T>(data, 0, width * height * depth);
        }

        /// <summary>
        /// Sets 3D texture data, specifying a start index and number of elements.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data">Array of data.</param>
        /// <param name="startIndex">Index of the first element to set.</param>
        /// <param name="elementCount">Number of elements to set.</param>
        public void SetData<T>(T[] data, int startIndex, int elementCount) where T : struct
        {
            if (elementCount != width * height * depth)
                throw new Exception("Data array is not the right size for texture data");
            if (startIndex < 0 || startIndex + elementCount > data.Length)
                throw new Exception("Index range does not lie within the data array");

            // Create a staging buffer to get CPU write access
            Texture3DDescription desc;
            SlimDX.Direct3D11.Texture3D dxstaging;
            desc = dxtexture.Description;
            desc.BindFlags = BindFlags.None;
            desc.CpuAccessFlags = CpuAccessFlags.Write;
            desc.Usage = ResourceUsage.Staging;
            dxstaging = new SlimDX.Direct3D11.Texture3D(Game.GraphicsDevice.DX_Device, desc);

            // Write data to staging buffer - note that the RowPitch and SlicePitch of the mapped
            // memory may not match the shape of the texture.  Where they do not, this code copies
            // the data to the staging buffer row by row.
            DataBox box = Game.GraphicsDevice.DX_Device_Context.MapSubresource(dxstaging, 0, 0, MapMode.Write, MapFlags.None);
            if (box.RowPitch == elementsize * width && box.SlicePitch == elementsize * width * height)
            {
                box.Data.WriteRange<T>(data, startIndex, elementCount);
            }
            else
            {
                int i = startIndex;
                for (int d = 0; d < depth; d++)
                {
                    for (int h = 0; h < height; h++)
                    {
                        box.Data.WriteRange<T>(data, i, width);
                        i += width;
                        if (box.RowPitch > width * elementsize)
                            box.Data.Seek(box.RowPitch - width * elementsize, System.IO.SeekOrigin.Current);
                    }
                    if (box.SlicePitch > box.RowPitch * height)
                        box.Data.Seek(box.SlicePitch - box.RowPitch * height, System.IO.SeekOrigin.Current);
                }
            }
            Game.GraphicsDevice.DX_Device_Context.UnmapSubresource(dxstaging, 0);

            // Transfer data to the texture2D
            Game.GraphicsDevice.DX_Device_Context.CopyResource(dxstaging, dxtexture);

            // Dispose of the unmanaged staging buffer structure
            dxstaging.Dispose();
        }

        /// <summary>
        /// Sets 3D texture data, specifying a mipmap level, source box, start index,
        /// and number of elements.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="level">Mipmap level.</param>
        /// <param name="left">X coordinate of the left face of the 3D bounding cube.</param>
        /// <param name="top">Y coordinate of the top face of the 3D bounding cube.</param>
        /// <param name="right">X coordinate of the right face of the 3D bounding cube.</param>
        /// <param name="bottom">Y coordinate of the bottom face of the 3D bounding cube.</param>
        /// <param name="front">Z coordinate of the front face of the 3D bounding cube.</param>
        /// <param name="back">Z coordinate of the back face of the 3D bounding cube.</param>
        /// <param name="data">Array of data.</param>
        /// <param name="startIndex">Index of the first element to set.</param>
        /// <param name="elementCount">Number of elements to set.</param>
        public void SetData<T>(int level, int xleft, int xtop, int xright, int xbottom, int xfront, int xback, T[] data, int startIndex, int elementCount) where T : struct
        {
            // Create a staging buffer to get CPU write access
            Texture3DDescription desc;
            int left = 0, top = 0, front = 0, wide, high, deep;
            SlimDX.Direct3D11.Texture3D dxstaging;
            desc = dxtexture.Description;
            CalculateLevelSize3D(level, width, height, depth, out wide, out high, out deep);

            left = xleft;
            top = xtop;
            front = xfront;
            if (left < 0 || left >= wide || top < 0 || top >= high || front < 0 || front >= deep ||
                xright > wide || xbottom > high || xback > deep)
                throw new Exception("Volume (" + left + "-" + xright + "," + top + "-" + xbottom + "," + front + "-" + xback
                                    + ") not inside mip level " + level + " of Texture3D(" + width + "," + height + "," + depth + ")");
            wide = xright - left;
            high = xbottom - top;
            deep = xback - front;

            if (elementCount != wide * high * deep)
                throw new Exception("Data provided('" + elementCount + ") is the wrong size for texture volume(" + (wide * high * deep) + ")");
            if (startIndex < 0 || startIndex + elementCount > data.Length)
                throw new Exception("Index range does not lie within the data array");
            desc.Width = wide;
            desc.Height = high;
            desc.Depth = deep;
            desc.MipLevels = 1;
            desc.BindFlags = BindFlags.None;
            desc.CpuAccessFlags = CpuAccessFlags.Write;
            desc.Usage = ResourceUsage.Staging;
            dxstaging = new SlimDX.Direct3D11.Texture3D(Game.GraphicsDevice.DX_Device, desc);

            // Write data to staging buffer - note that the RowPitch and SlicePitch of the mapped
            // memory may not match the shape of the texture.  Where they do not, this code copies
            // the data to the staging buffer row by row.
            DataBox box = Game.GraphicsDevice.DX_Device_Context.MapSubresource(dxstaging, 0, 0, MapMode.Write, MapFlags.None);
            if (box.RowPitch == elementsize * width && box.SlicePitch == elementsize * width * height)
            {
                box.Data.WriteRange<T>(data, startIndex, elementCount);
            }
            else
            {
                int i = startIndex;
                for (int d = 0; d < deep; d++)
                {
                    for (int h = 0; h < high; h++)
                    {
                        box.Data.WriteRange<T>(data, i, wide);
                        i += wide;
                        if (box.RowPitch > width * elementsize)
                            box.Data.Seek(box.RowPitch - wide * elementsize, System.IO.SeekOrigin.Current);
                    }
                    if (box.SlicePitch > box.RowPitch * height)
                        box.Data.Seek(box.SlicePitch - box.RowPitch * height, System.IO.SeekOrigin.Current);
                }
            }
            Game.GraphicsDevice.DX_Device_Context.UnmapSubresource(dxstaging, 0);

            // Transfer data from the staging buffer to the texture2D
            Game.GraphicsDevice.DX_Device_Context.CopySubresourceRegion(dxstaging, 0,
                                                                        new ResourceRegion(0, 0, 0, wide, high, deep),
                                                                        dxtexture, level, left, top, front);

            // Dispose of the unmanaged staging buffer structure
            dxstaging.Dispose();
        }

        public void Dispose()
        {
            if (!this_wraps_an_external_texture)
            {
                dxtextureview.Dispose();
                dxtexture.Dispose();
            }
        }

        protected override Texture3D Read(ContentReader input, Texture3D existingInstance)
        {
            throw new NotImplementedException();
        }
    }
}
